/**
 * Copyright (C) 2010-2018 Gordon Fraser, Andrea Arcuri and EvoSuite
 * contributors
 *
 * This file is part of EvoSuite.
 *
 * EvoSuite is free software: you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published
 * by the Free Software Foundation, either version 3.0 of the License, or
 * (at your option) any later version.
 *
 * EvoSuite is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with EvoSuite. If not, see <http://www.gnu.org/licenses/>.
 */
package org.evosuite.continuous.job.schedule;

import org.evosuite.continuous.job.JobDefinition;
import org.evosuite.continuous.job.JobScheduler;
import org.evosuite.continuous.project.ProjectStaticData;
import org.evosuite.continuous.project.ProjectStaticData.ClassInfo;
import org.evosuite.utils.LoggingUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
import java.util.List;

/**
 * <p>
 * HistorySchedule class.
 * </p>
 * 
 * @author José Campos
 */
public class HistorySchedule extends OneTimeSchedule {

  private static double MODIFIED = 2.0;

  private static double NOT_MODIFIED = 1.0;

  public static final int COMMIT_IMPROVEMENT = 3;

  public HistorySchedule(JobScheduler scheduler) {
    super(scheduler);
  }

  @Override
  protected List<JobDefinition> createScheduleOnce() {

    ProjectStaticData data = this.scheduler.getProjectData();

    int maximumBudgetPerCore = 60 * this.scheduler.getConfiguration().timeInMinutes;

    // the total budget we need to choose how to allocate
    int totalBudget =
        maximumBudgetPerCore * this.scheduler.getConfiguration().getNumberOfUsableCores();

    // a part of the budget is fixed, as each CUT needs a minimum of it
    int minTime = 60 * this.scheduler.getConfiguration().minMinutesPerJob
        * data.getTotalNumberOfTestableCUTs();

    // this is what left from the minimum allocation, and that now we can
    // choose how best to allocate
    int extraTime = totalBudget - minTime;

    // check how much time we can give extra for each branch in a CUT
    int number_of_branches = data.getTotalNumberOfBranches();
    double timePerBranch =
        number_of_branches == 0.0 ? 0.0 : (double) extraTime / (double) number_of_branches;

    List<ClassInfo> classesInfo = new ArrayList<ClassInfo>(data.getClassInfos());

    // classes that have been changed first
    Collections.sort(classesInfo, new Comparator<ClassInfo>() {
      @Override
      public int compare(ClassInfo a, ClassInfo b) {
        if (a.hasChanged() && !b.hasChanged()) {
          return -1;
        } else if (!a.hasChanged() && b.hasChanged()) {
          return 1;
        }

        // otherwise, get the most difficult classes first
        return Integer.compare(b.numberOfBranches, a.numberOfBranches);
      }
    });

    int totalLeftOver = 0;
    int totalBudgetUsed = 0;
    List<JobDefinition> jobs = new LinkedList<JobDefinition>();

    for (ClassInfo c_info : classesInfo) {
      if (!c_info.isTestable()) {
        continue;
      }
      if (!c_info.hasChanged() && !c_info.isToTest()) {
        LoggingUtils.getEvoLogger().info("- Skipping class " + c_info.getClassName()
            + " because it does not seem to be worth it");
        continue;
      }

      double budget = 60.0 * scheduler.getConfiguration().minMinutesPerJob
          + (c_info.numberOfBranches * timePerBranch);

      // classes that have been modified could get more time than 'normal' classes
      budget *= c_info.hasChanged() ? HistorySchedule.MODIFIED : HistorySchedule.NOT_MODIFIED;

      if (budget > maximumBudgetPerCore) {
        /*
         * Need to guarantee that no job has more than maximumBudgetPerCore regardless of number of
         * cores
         */
        totalLeftOver += (budget - maximumBudgetPerCore);
        budget = maximumBudgetPerCore;
      }

      if ((totalBudgetUsed + budget) <= totalBudget) {
        totalBudgetUsed += budget;

        LoggingUtils.getEvoLogger()
            .info("+ Going to generate test cases for " + c_info.getClassName()
                + " using a time budget of " + budget + " seconds. Status of it ["
                + (c_info.hasChanged() ? "modified" : "not modified") + "]");

        jobs.add(new JobDefinition((int) budget,
            this.scheduler.getConfiguration().getConstantMemoryPerJob(), c_info.getClassName(), 0,
            null, null));
      } else {
        LoggingUtils.getEvoLogger()
            .info("- There is not enough time budget to test " + c_info.getClassName()
                + ". Status of it [" + (c_info.hasChanged() ? "modified" : "not modified") + "]");

        // mark this CUT as not tested. this is useful to later on distinguish
        // classes that EvoSuite failed to generate test cases and classes that
        // we didn't actually create any job for
        c_info.isToTest(false);
      }
    }

    totalLeftOver += (totalBudget - totalBudgetUsed);

    /*
     * do we still have some more budget to allocate? and is it less
     * than totalBudget? (because if it is equal to totalBudget, means
     * that we have skipped all classes, and therefore there is not point
     * of distributing left over budget as all classes will be skipped)
     */
    if (totalLeftOver > 0 && totalLeftOver < totalBudget) {
      LoggingUtils.getEvoLogger().info("Distributing left budget (" + totalLeftOver + ")");
      distributeExtraBudgetEvenly(jobs, totalLeftOver, maximumBudgetPerCore);
    }

    return jobs;
  }
}
